/* 16x2 LCD Spectrum Analyzer Adapted for: Elecrow All-in-One Starter Kit for Arduino Nano R4 Hardware connections on the kit: -------------------------------- Microphone / Sound sensor : A1 Three buttons : A3 LCD SDA : A4 LCD SCL : A5 LCD I2C address : 0x27 K1 changes the spectrum display style. by mircemk July 2026 */ #include #include #include // ===================================================== // ELECROW KIT PINS // ===================================================== #define AUDIO_PIN A1 #define BUTTON_PIN A3 // ===================================================== // LCD CONFIGURATION // Same configuration as the official Elecrow LCD code // ===================================================== #define LCD_COLUMNS 16 #define LCD_ROWS 2 LiquidCrystal_I2C lcd( PCF8574_ADDR_A21_A11_A01, 4, 5, 6, 16, 11, 12, 13, 14, POSITIVE ); // ===================================================== // FFT CONFIGURATION // ===================================================== const uint16_t SAMPLES = 256; // 8000 Hz gives a maximum detectable frequency of 4000 Hz. const double SAMPLING_FREQUENCY = 8000.0; double vReal[SAMPLES]; double vImag[SAMPLES]; ArduinoFFT FFT = ArduinoFFT( vReal, vImag, SAMPLES, SAMPLING_FREQUENCY ); // FFT bins used for the 16 LCD columns. // With 8000 Hz / 256 samples: // one FFT bin is approximately 31.25 Hz. const uint8_t spectrumBins[16] = { 2, 3, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30 }; // ===================================================== // SPECTRUM SETTINGS // ===================================================== // Increase this if the display reacts to background noise. // Decrease it if the spectrum reacts too weakly. const double NOISE_FLOOR_DB = 20.0; // Minimum dynamic range above the noise floor. const double MIN_DYNAMIC_RANGE_DB = 8.0; // Additional space above the strongest peak. const double PEAK_HEADROOM_DB = 4.0; // Spectrum movement smoothing. // Higher value = faster movement. // Lower value = smoother movement. const float BAR_ATTACK = 0.95; const float BAR_RELEASE = 0.55; // Automatic gain smoothing. const float AUTO_GAIN_SPEED = 0.12; // Displayed bar levels: 0 to 15. float displayedLevel[16]; // Automatically adjusted upper spectrum limit. double autoGainTopDB = 55.0; // ===================================================== // DISPLAY MODES // ===================================================== // Six pixel patterns from the original project. const uint8_t modePattern[6] = { 4, // 00100 14, // 01110 10, // 01010 27, // 11011 31, // 11111 21 // 10101 }; uint8_t spectrumMode = 0; // Button debounce / release detection. bool buttonWasPressed = false; unsigned long lastButtonTime = 0; // ===================================================== // FUNCTION PROTOTYPES // ===================================================== void createSpectrumCharacters(); void readModeButton(); bool isK1Pressed(); void collectAudioSamples(); void calculateSpectrum(); void drawSpectrum(); double magnitudeToDb(double magnitude); // ===================================================== // SETUP // ===================================================== void setup() { Serial.begin(115200); pinMode(AUDIO_PIN, INPUT); pinMode(BUTTON_PIN, INPUT); // Default analogRead resolution used by the official // Elecrow button example is 10 bits: 0 to 1023. analogReadResolution(10); Wire.begin(); lcd.begin(LCD_COLUMNS, LCD_ROWS, LCD_5x8DOTS); lcd.clear(); lcd.backlight(); createSpectrumCharacters(); lcd.setCursor(0, 0); lcd.print("Spectrum"); lcd.setCursor(0, 1); lcd.print("Analyzer Nano R4"); delay(1200); lcd.clear(); // Initial ADC read after selecting A1. // This helps the ADC input settle. analogRead(AUDIO_PIN); } // ===================================================== // MAIN LOOP // ===================================================== void loop() { readModeButton(); collectAudioSamples(); calculateSpectrum(); drawSpectrum(); } // ===================================================== // READ K1 BUTTON ON A3 // ===================================================== void readModeButton() { bool pressed = isK1Pressed(); if (pressed && !buttonWasPressed) { if (millis() - lastButtonTime > 250) { spectrumMode++; if (spectrumMode > 5) { spectrumMode = 0; } createSpectrumCharacters(); lastButtonTime = millis(); } buttonWasPressed = true; } if (!pressed) { buttonWasPressed = false; } } bool isK1Pressed() { /* Read twice because the ADC was previously reading A1. The first read allows the ADC multiplexer to settle. */ analogRead(BUTTON_PIN); int buttonValue = analogRead(BUTTON_PIN); /* Official Elecrow values: K1 approximately 500-520 K2 approximately 680-690 K3 approximately 845-860 A slightly wider range is used for tolerance. */ if (buttonValue >= 470 && buttonValue <= 550) { return true; } return false; } // ===================================================== // CREATE THE 8 CUSTOM LCD BAR CHARACTERS // ===================================================== void createSpectrumCharacters() { uint8_t pixelPattern = modePattern[spectrumMode]; for (uint8_t characterNumber = 0; characterNumber < 8; characterNumber++) { uint8_t customCharacter[8]; /* Character 0 has one active row. Character 7 has all eight rows active. */ for (uint8_t row = 0; row < 8; row++) { if (row >= (7 - characterNumber)) { customCharacter[row] = pixelPattern; } else { customCharacter[row] = 0; } } lcd.createChar(characterNumber, customCharacter); } } // ===================================================== // AUDIO SAMPLING // ===================================================== void collectAudioSamples() { const uint32_t samplingPeriodMicroseconds = 1000000UL / SAMPLING_FREQUENCY; // Allow ADC multiplexer to settle after reading A3. analogRead(AUDIO_PIN); for (uint16_t i = 0; i < SAMPLES; i++) { uint32_t sampleStart = micros(); vReal[i] = analogRead(AUDIO_PIN); vImag[i] = 0.0; while ((micros() - sampleStart) < samplingPeriodMicroseconds) { // Wait for the next sample time. } } } // ===================================================== // FFT CALCULATION // ===================================================== void calculateSpectrum() { // Remove microphone DC offset. FFT.dcRemoval(); // Apply Hamming window. FFT.windowing( FFTWindow::Hamming, FFTDirection::Forward ); // Calculate FFT. FFT.compute(FFTDirection::Forward); // Convert complex values into magnitudes. FFT.complexToMagnitude(); double frameMaximumDB = NOISE_FLOOR_DB; // Find strongest displayed frequency bin. for (uint8_t column = 0; column < 16; column++) { uint8_t bin = spectrumBins[column]; double levelDB = magnitudeToDb(vReal[bin]); if (levelDB > frameMaximumDB) { frameMaximumDB = levelDB; } } // Calculate target upper gain limit. double targetTopDB = frameMaximumDB + PEAK_HEADROOM_DB; if (targetTopDB < NOISE_FLOOR_DB + MIN_DYNAMIC_RANGE_DB) { targetTopDB = NOISE_FLOOR_DB + MIN_DYNAMIC_RANGE_DB; } // Smooth automatic gain. autoGainTopDB = autoGainTopDB * (1.0 - AUTO_GAIN_SPEED) + targetTopDB * AUTO_GAIN_SPEED; // Calculate 16 display levels. for (uint8_t column = 0; column < 16; column++) { uint8_t bin = spectrumBins[column]; double levelDB = magnitudeToDb(vReal[bin]); double normalized = (levelDB - NOISE_FLOOR_DB) / (autoGainTopDB - NOISE_FLOOR_DB); if (normalized < 0.0) { normalized = 0.0; } if (normalized > 1.0) { normalized = 1.0; } float targetLevel = normalized * 15.0; // Faster rise, slower fall. if (targetLevel > displayedLevel[column]) { displayedLevel[column] = displayedLevel[column] * (1.0 - BAR_ATTACK) + targetLevel * BAR_ATTACK; } else { displayedLevel[column] = displayedLevel[column] * (1.0 - BAR_RELEASE) + targetLevel * BAR_RELEASE; } } } // ===================================================== // CONVERT FFT MAGNITUDE TO DECIBEL-LIKE VALUE // ===================================================== double magnitudeToDb(double magnitude) { return 20.0 * log10(magnitude + 1.0); } // ===================================================== // DRAW 16 SPECTRUM BARS // ===================================================== void drawSpectrum() { for (uint8_t column = 0; column < 16; column++) { int level = round(displayedLevel[column]); level = constrain(level, 0, 15); if (level == 0) { lcd.setCursor(column, 0); lcd.print(' '); lcd.setCursor(column, 1); lcd.print(' '); } else if (level <= 8) { lcd.setCursor(column, 0); lcd.print(' '); lcd.setCursor(column, 1); lcd.write((uint8_t)(level - 1)); } else { lcd.setCursor(column, 0); lcd.write((uint8_t)(level - 9)); lcd.setCursor(column, 1); lcd.write((uint8_t)7); } } }